E-이스티오의 데이터 플레인 트래픽 세팅 원리
개요
외부의 클라이언트가 게이트웨이로 요청을 보내 워크로드 파드로 전송되고, 파드 내에서 엔보이를 거쳐 어플리케이션에 도달해 돌아가는 과정.
이 내용은 쿠버네티스 클러스터에서 트래픽이 전달되는 과정까지 아우르는 굉장히 넓은 범위를 포괄한다.
나는 구체적으로 엔보이와 어플리케이션이 통신하고 트래픽이 움직이는 과정을 탐구하고자 한다.
세팅
클러스터에서 파드를 띄우고 이를 추적해보는 것은 상당히 번거롭다.
왜냐하면 컨테이너 기술은 네임스페이스를 분리시켜 호스트와 다른 네트워크인 것 같은 환경을 구성하기에 일차적인 격리가 발생하기 때문이다.
여기에 컨테이너 보안을 위해 컨테이너에서는 트래픽을 캡쳐하고 추적하는데 필요한 각종 관리자 권한을 막는 경우가 많아 트래픽을 추적하기 위해 들어가는 세팅이 상당하다.
그런데 이러한 컨테이너 환경에서의 불편함을 전부 깡그리 무시하고 이스티오의 트래픽을 캡쳐하는 방법이 있으니..
앞서 다룬 가상머신을 활용하는 것이다!
관련한 내용은 E-이스티오 가상머신 통합에 담겨 있으니 참고!
사전 지식
컨트랙
Connection Tracking, 줄여서 컨트랙은 넷필터 내부의 stateful 모듈이다.
이름 그대로 연결을 추적하는 역할을 하는데, 이것 덕분에 iptables는 stateful한 방화벽으로 기능할 수 있다.
iptables 이전에 ipchains라는 건 상태 추적이 없었다고 한다.[1]
컨트랙이 활성화돼있고 각 테이블들이 기본 정책으로 ACCEPT일 경우, 바운드되는 패킷은 nat 등의 룰의 적용을 받지 않는다.
그래서 iptables 룰을 설정하는 관점에서 응답 패킷에 대한 설정을 할 필요가 없어지므로 설정이 간소화되고 복잡성이 줄어든다.
자세한 정보는 따로 문서를 파서 정리할 것 같다.
참고하기 좋은 문서.[2]
메모리에 정보를 저장하고 활용한다.
cat /proc/slabinfo
이걸로 메모리 사이즈 확인 가능.
/var/log/dmesg
여기에서 ip_conntrack 부분을 보면 한 컨트랙이 얼마의 메모리 바이트를 차지하는지도 나오는데, 기본은 208이다.
항상 이게 모듈로 올라와있는 건 아니라고 한다.
있을 때 iptables에서 활용할 수 있게 되는 것이다.
cat /proc/sys/net/ipv4/ip_conntrack_max
이걸로 최대 테이블 개수 확인 가능.
기본은 32768개이다.
위에서 한 컨트랙 메모리 바이트가 208이었으니, 최대로는 메모리 6메가를 활용하게 된다.
테이블이 꽉 차면 그 이상의 패킷에 대해 상태 추적을 안 할 것 같지만, 실상은 아예 패킷을 드랍시켜버린다고 한다.
간혹 다량의 트래픽이 발생하는 노드에서 ssh 연결이 이유 없이 끊긴다면 이게 이슈일 가능성이 높다.
sysctl -w net.ipv4.ip_conntrack_max=100000
echo 100000 > /proc/sys/net/ipv4/ip_conntrack_max
두 명령어 모두 테이블 개수를 설정할 수 있다.
프록시와 워크로드의 트래픽
서비스 메시의 아이디어는 프록시를 둬서 어플리케이션으로 드나드는 트래픽을 가로챈다는 것이고, 이스티오에서는 엔보이가 해당 프록시 역할을 수행한다.
여기에서 트래픽을 보내고 받는 대상을 규정하자면 외부, 엔보이, 어플리케이션이라 할 수 있다.
하지만 iptables를 보기 위해서는 패킷 관점에서 접근할 필요가 있다.
- 외부에서 엔보이로 들어가는 트래픽
- 엔보이에서 어플리케이션으로 가는 트래픽
- 어플리케이션에서 엔보이로 들어가는 트래픽
- 엔보이에서 외부로 나가는 트래픽
이걸로 끝인가?
아니, 모든 트래픽은 요청이 갔다가 돌아오는 응답이 있기 마련이므로, 위 4가지 분류 상의 트래픽은 각각 그에 상응하는 응답 트래픽도 존재한다.
이것을 왜 분류해야 하는가?
iptables은 엔보이와 어플리케이션이 돌아가는 프로세스 밑단에서 네트워크를 필터링하고 처리하기 때문이다.
잠시 언급한 컨트랙을 통해 대부분의 응답 트래픽이 잘 처리되긴 하나, 이스티오에서는 DNS 관련하여 컨트랙 관련 추가 룰을 작성한다.
이 부분에 대해서 아래에서 더 자세히 보겠다.
아무튼 요청 이후에는 항상 응답 패킷도 있다는 것은 염두해두자.
system-resolved
추가적으로, 실습을 진행한 환경은 우분투로, 우분투는 기본적으로 system-resolved라는 데몬이 활성화돼있다.
이 데몬은 외부 네임 서버에 질의를 해주는 대리자로, 로컬에서의 모든 DNS질의는 일단 이 데몬을 거친다.
로컬의 질의는 /etc/resolve.conf
에 지정된 주소로 날아가는데 여기에 127.0.0.53:53으로 system-resolved의 주소가 적혀있다.
이것 때문에 패킷 추적에 조금 어려움이 있긴 한데, 이렇게 리졸버 데몬이 띄워둔 환경에서도 DNS 프록시 기능은 잘 작동한다.
iptables 분석
지미 송이 도식화한 파드 내에서 전체 트래픽이 움직이는 과정이다.[3]
iptables로 조작되는 트래픽의 흐름이 상세하게 적혀있다.
전체 도식을 한번에 표현해서 되려 복잡하게 느껴지기도 한다..
이것만으로 모든 규칙과 흐름이 이해된다면, 이 글은 읽을 필요 없다.
적용된 iptables의 상태를 간단하게 덤프 떠서 보자.
iptables-save
요런 식으로 확인할 수 있다.
mangle 테이블에 dns 관련 체인이 들어가는 것도 확인할 수 있다.
테이블 형태로 확인하는 것이 그래도 조금 더 보기는 편하지 않은가 싶다.
iptables -t nat -L -n -v
iptables -t raw -L -n -v
단순하게 테이블 별로 체인 리스트를 보는 명령어이다.
n 옵션으로 웰논 포트와 주소가 대체되지 않도록 했는데, 제대로 규칙을 확인하려면 여기에 꼭 v 인자를 넣어야 한다.
그래야만 드나드는 인터페이스 기준도 보이기 때문이다.
(이거 빼고 봐도 규칙 자체는 풀로 보이는 줄 알고 헤맸다..)
이제부터 이걸 본격적으로 탐구해본다.
패킷 경로를 정하는, 가장 기본이 되는 nat 테이블은 이런 모양을 가지고 있다.
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
281 15320 ISTIO_INBOUND tcp -- * * 0.0.0.0/0 0.0.0.0/0
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
6341 420K ISTIO_OUTPUT all -- * * 0.0.0.0/0 0.0.0.0/0
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain ISTIO_INBOUND (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15008
2 148 RETURN tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
0 0 RETURN tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15090
0 0 RETURN tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15021
0 0 RETURN tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:15020
279 15172 ISTIO_IN_REDIRECT tcp -- * * 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_IN_REDIRECT (3 references)
pkts bytes target prot opt in out source destination
280 15232 REDIRECT tcp -- * * 0.0.0.0/0 0.0.0.0/0 redir ports 15006
Chain ISTIO_OUTPUT (1 references)
pkts bytes target prot opt in out source destination
4261 256K RETURN all -- * lo 127.0.0.6 0.0.0.0/0
1 60 ISTIO_IN_REDIRECT tcp -- * lo 0.0.0.0/0 !127.0.0.1 multiport dports !53,15008 owner UID match 998
9 540 RETURN tcp -- * lo 0.0.0.0/0 0.0.0.0/0 tcp dpt:!53 ! owner UID match 998
146 10460 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 owner UID match 998
0 0 ISTIO_IN_REDIRECT tcp -- * lo 0.0.0.0/0 !127.0.0.1 tcp dpt:!15008 owner GID match 998
0 0 RETURN tcp -- * lo 0.0.0.0/0 0.0.0.0/0 tcp dpt:!53 ! owner GID match 998
0 0 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 owner GID match 998
1924 153K ISTIO_OUTPUT_DNS all -- * * 0.0.0.0/0 0.0.0.0/0
81 9698 RETURN all -- * * 0.0.0.0/0 127.0.0.1
1768 137K ISTIO_REDIRECT all -- * * 0.0.0.0/0 0.0.0.0/0
뭔가 여러 룰들이 들어있는 것이 보이는데, 얼추 보면 인바운드와 아웃바운드에 대한 규칙들이 작성됐음을 확인할 수 있다.
raw 테이블은 어떻게 생겼는가?
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
ISTIO_INBOUND all -- 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
ISTIO_OUTPUT_DNS all -- 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_INBOUND (1 references)
target prot opt source destination
CT udp -- 127.0.0.53 0.0.0.0/0 udp spt:53 CT zone 1
Chain ISTIO_OUTPUT_DNS (1 references)
target prot opt source destination
CT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:53 owner UID match 998 CT zone 1
CT udp -- 0.0.0.0/0 0.0.0.0/0 udp spt:15053 owner UID match 998 CT zone 2
CT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:53 owner GID match 998 CT zone 1
CT udp -- 0.0.0.0/0 0.0.0.0/0 udp spt:15053 owner GID match 998 CT zone 2
CT udp -- 0.0.0.0/0 127.0.0.53 udp dpt:53 CT zone 2
raw 테이블에서는 dns 관련 컨트랙을 설정하는 룰이 들어있다.
각 룰의 맨 끝에 CT zone 1, 2라고 설정하는 것이 컨트랙의 지역을 설정하는 것으로, 임의의 어떤 트래픽들을 같은 트래픽으로 추적되지 않고 두 가지로 분리하여 인식할 수 있도록 추가적인 설정을 하는 것이다.
역시 그냥 보면 좀 어렵다고 생각해서, 각 상황 별로 발생하는 트래픽이 어떤 흐름을 타게 되는지 단계 별로 구조화해본다.
참고로 현 인스턴스에서 istio-proxy는 998 UID, GID를 가지고 있는데, 클러스터에 배치되는 프록시를 보면 1337을 UID, GID로 고정적으로 가진다.
룰에 걸리는 패킷의 변화를 유동적으로 추적하고 싶다면 이렇게 watch를 걸어두는 것을 추천한다.
CHAIN="PREROUTING ISTIO_INBOUND ISTIO_IN_REDIRECT ISTIO_OUTPUT ISTIO_OUTPUT_DNS ISTIO_REDIRECT"
watch -d "for i in $CHAIN ;do iptables -t nat -n -v -L \$i; echo; done"
CHAIN 문 안에 원하는 체인의 이름을 나열해주면 된다.
INPUT 같은 체인은 볼 필요가 없어서 빼기 위해서 이렇게 사용했다.
조금이라도 간소화시켜 작성하기 위해 도식에 색을 사용했다.
리턴 타겟을 가진 룰은 빨간색으로 해당 체인을 빠져나가 이전 체인으로 돌아가 룰 적용을 받는다.
파란색으로 작성한 룰은 별도의 조건이 명시되지 않아 해당 룰에 걸리는 패킷은 모두 적용을 받게 된다.
nat 테이블 - PREROUTING 체인
먼저 들어오는 트래픽이 어떻게 처리되는지 확인해보자.
PREROUTING 체인에 걸리는 패킷은 다음의 순서를 거친다.
사실 이쪽은 그렇게 어렵지 않다.
간단하게 말해 위의 몇 가지 포트를 목적지로 하는 패킷들만 그냥 진행되고, 나머지 모든 패킷들은 엔보이의 인바운드 핸들러 포트로 전달된다.
HBONE은 HTTP-Based Overlay Network Environment라 하여, 이스티오 컴포넌트 간 보안 터널링 프로토콜을 말한다.
여러 TCP 스트림을 하나의 mtls 커넥션으로 처리하기 위해 존재하며, 해당 통신 포트가 15008이다.
앰비언트 모드에서만 쓰이는 건지는 이후 앰비언트 모드까지 공부해야 명확해지겠다.[4]
이후에 INPUT 체인도 거치게 될 텐데, 해당 부분에는 아무런 체인이 걸려있지 않으므로 그대로 통과되기에 기재하지 않았다.
nat 테이블 - OUTPUT 체인
사실 나가는 트래픽 처리가 꽤 복잡하다.
위에서 말했듯이 나가는 트래픽을 세분화시켜야 하기 때문이다.
처음 봤을 때는 이게 뭐지 싶었는데, 7번에 있는 가장 마지막 룰에 적용을 받기 이전에 걸러낼 트래픽들을 걸러내는 과정이라고 생각하면 한결 편하다.
7번은 15001포트, 즉 엔보이가 나가는 트래픽을 처리하는 아웃바운드 핸들러로 패킷을 보내는 규칙이다.
그러니 엔보이가 보내는 패킷이 7번에 도달하면 다시 패킷이 엔보이로 들어가는 불상사가 발생할 것이다!
여러 상세한 규칙이 설정된 게 바로 이러한 이유 때문이다..
각 번호의 룰이 담당하는 의미를 분석해보자.
- 1 - 엔보이에서 어플리케이션으로 가는 패킷
- 127.0.0.6은 엔보이가 앱으로 트래픽을 보낼 때 사용하는 IP 주소로, InboundPassthroughClusterIpv4라고 부른다.
- 이 이름은 엔보이 설정에서 다시금 보게 될 것이다.
- 엔보이가 보내는데 이게 다시 엔보이로 들어가지 않고 어플리케이션으로 갈 수 있도록 바로 리턴한다.
- 127.0.0.6은 엔보이가 앱으로 트래픽을 보낼 때 사용하는 IP 주소로, InboundPassthroughClusterIpv4라고 부른다.
- 2 - 어플리케이션에서 127.0.0.1 주소를 이용하지 않으면서 자기 자신을 부르는 패킷
- 딱 봐도 이 놈이 가장 복잡하다...
- 이 룰에 걸리는 패킷은 어플리케이션이 자기 자신을 호출할 때 생긴다.
- 만약 localhost(127.0.0.1)을 이용했다면 여기에 걸리지 않는다.
- 어플리케이션이 localhost가 아니라 자신의 서비스 도메인이나 ip를 이용해 호출했다면, 해당 트래픽은 엄연하게 서비스 메시 내부에서 발생하는 트래픽으로 간주해야 한다.
- 그렇기에 메시에서 설정되는 신원을 받고, 각종 정책의 적용을 받을 필요가 있다.
- 그래서 해당 트래픽을 ISTIO_IN_REDIRECT, 즉 인바운드 핸들러인 15006포트로 보내는 것이다.
- 이 부분에 대해 조금 더 아래에서 부연하겠다.
- 3 - 내부 통신 처리
- 기본적으로 localhost를 이용하는 내부 통신은 그냥 리턴하여 엔보이를 거치지 않도록 한다.
- 4 - 엔보이에서 외부로 보내는 패킷
- 어플리케이션에서 외부로 보내는 패킷을 엔보이가 외부로 보낼 때 바로 리턴한다.
- 1번에서 리턴될 패킷도 이 조건에 매칭될 텐데, 2번 룰 때문에 선제적인 조건을 건 것으로 보인다.
- 5 - 어플리케이션의 DNS 패킷
- 여기까지 온 패킷은 전부 ISTIO_OUTPUT_DNS 체인으로 들어간다.
- 그런데 해당 체인에 127.0.0.53:53, 즉 로컬 리졸버를 목적지 주소로 삼을 때만 리디렉션되도록 돼있다.
- 결과적으로 DNS 질의 패킷만이 15053으로 보내지고 나머지 패킷은 돌아와 6번 룰의 평가를 받을 것이다.
- 6 - 에이전트가 보낸 DNS 질의에 응답하는 패킷
- 조건은 그냥 127.0.0.1에 해당하면 무조건 리턴하는 것으로 나와있다.
- 근데 3번 룰에서 실상 내부 통신은 이미 돼서, 여길 거치는 패킷이 거의 없다..
- 근데 에이전트가 시스템 리졸버에 날린 질의의 응답이 이 룰에 걸린다.
- 돌아가는 응답은 127.0.0.53의 주소에서 127.0.0.1로 가기 때문이다.
- 7 - 어플리케이션에서 엔보이로 가는 패킷
- 위 조건에 전부 해당하지 않는 모든 패킷은 어플리케이션이 외부로 보내는 요청으로서 처리된다.
- 그래서 아웃바운드 핸들러 포트인 15001로 리다이렉트된다.
분석을 해봐도, 사실 엄청 명쾌하진 않다.
2번 규칙이 사실 이 모든 걸 어렵게 만드는 포인트라고 생각하는데, 아무튼 발생하는 트래픽 케이스 별로 아래에서 흐름도를 다시 정리하겠다.
raw 테이블
마지막으로 DNS의 컨트랙을 관리하는 raw 테이블을 보도록 한다.
raw 테이블에서는 컨트랙을 논리적으로 구분 짓기 위해 zone 설정을 한다. 컨트랙이 제대로 연결되면 요청 패킷에 대한 응답 패킷이 nat 테이블의 적용을 받지 않기 때문에 룰 설정이 상대적으로 간편해진다. 컨트랙은 `프로토콜:출발지 주소:출발치 포트:목적지 주소: 목적지 포트`로 대칭되는 패킷을 결정짓는데, 이스티오가 아무리 장난질을 쳐도 애플리케이션과 통신할 때는 127.0.0.6 주소를 쓴다던가 하는 식으로 차별을 나름 두기 때문에 컨트랙에 문제가 발생하지 않는다.문제가 생길 여지가 있는 건 바로 DNS 질의를 할 때이다.
혼자 머리를 싸매며 패킷 경로를 추적해서 원인을 추론해낸 거라 정확하지 않을 수 있다.
아직 넷필터 모듈의 전반적인 동작을 잘 아는 게 아니라 오히려 틀릴 가능성이 있다는 것도 염두해둔다.
아무튼 도식 상에서는 zone을 딱 2개로 나눈다.
그래도 DNS의 작업은 결국 크게 두 가지로 나뉘기 때문에 엄청 어렵진 않다.
- 어플리케이션 to 에이전트
- 해당 요청은 127.0.0.53:53을 향해 출발하며 ISTIO_OUTBOUND_DNS의 가장 마지막 룰에 의해 zone1로 기록된다.
- 그리고 nat 테이블의 적용을 받아 살짝쿵 에이전트가 DNS 프록싱을 위해 열어둔 15053포트로 리다이렉트된다.
- NDS를 통하든, 시스템 리졸버를 통하든 에이전트는 53포트인 것 마냥 응답을 돌려보내는데 이때 중간 룰에 걸려 zone1로 기록된다.
- 에이전트 to 시스템 리졸버 or 외부 네임 서버
- 해당 요청은 127.0.0.53:53을 향해 출발하는데 istio-proxy의 GID에 매칭되어 첫번째 룰에 걸려 zone 2로 기록된다.
- 이후 리졸버가 resolve.conf에 설정된 네임서버로 질의를 날리고, 돌아오는 패킷이 PREROUTING에 걸린다.
- 이 패킷은 출발지가 127.0.0.53:53일 것이며, zone2로 기록된다.
결론적으로 어플리케이션이 에이전트에 보내는 DNS 질의는 zone 1, 에이전트가 리졸버에 보내는 질의는 zone 2로 기록되는 것이다.
상황 별 패킷 흐름
그럼 실제 트래픽이 지나가는 경로를 명확하게 밝혀보자.
패킷의 흐름을 조금 더 시각적으로 확인하기 위해 와이어샤크를 활용했다.
# vm에서
tcpdump -i any -c 1000 -w /home/ubuntu/capture1.pcap
dig +short naver.com
# 로컬에서
scp -i infra-terraform/key.pem -r ubuntu@$FORUM:/root/capture.pcap .
처음에는 termshark를 이용해 터미널 환경에서 분석을 했지만, 시각화가 자유롭지는 않은 것 같다고 느껴서 덤프 파일을 가져오는 식으로 진행했다.
DNS 질의 트래픽
클러스터 도메인으로 보내는 트래픽은 에이전트에서 모든 응답을 마치기 때문에 그다지 확인할 지점이 없다고 판단하여 외부 도메인 질의를 해봤다.
참고로 시스템 리졸버라는 놈도 사실은 로컬 환경의 dns 질의를 앞단에서 처리해주는 데몬이라 거의 프록시 느낌이다.
현 인스턴스에서 실제 답변을 진행하는 네임서버는 192.168.0.2에 있다.
dig +short github.com
먼저 패킷 캡쳐 그림이다.
총 세 번의 질의가 있었고, 스택에서 원소를 꺼내듯이 응답이 돌아갔다.
- 127.0.0.1:47044 -> 127.0.0.1:15053
- 터미널 세션에서 날린 127.0.0.53:53으로의 요청이 iptables를 거쳐 15053포트로 향하게 됐다.
- 이때 주의할 점은, tcpdump는 인터페이스를 통과하는 시점의 패킷을 검사한다는 것이다.
- 로컬에서 출발한 트래픽은 인터페이스를 지나기 전에 output 체인을 거치기에 DNAT됐다.
- 127.0.0.1:47041 -> 127.0.0.53:53
- 패킷 도둑넘이 대답할 수 없는 도메인이라 시스템 리졸버에게 다시 질의
- 192.168.10.200:34602 -> 192.168.0.2:53
- 시스템 리졸버가 외부 네임서버에 질의
그리고 거꾸로 응답이 돌아왔다.
이 흐름 자체는 위의 과정을 생각해보면 얼추 당연할 것이다.
그렇다면 iptables 상에서는 어떻게 요청 패킷이 가고 응답 패킷이 돌아올까?
다른 것들도 일일히 도식을 그려본 바로는, 사실 DNS 질의 추적이 가장 어렵다..
먼저 질의 한번에 영향을 받은 룰을 살펴보자면,
이렇게 여러 룰이 한꺼번에 카운트되는 것을 확인할 수 있다!
처음에 이걸 어떻게 해석해야 하나 많이 난감했는데, 구체적으로 따져보니 이 정도로 도식을 그릴 수 있겠다.
(OUTPUT 체인은 너무 당연해서 표기하지 않았다.)
ISTIO_OUTPUT 체인을 보면 4가지 룰에 패킷 카운트가 되는 것을 확인할 수 있는데, 이들을 보라색으로 표기했다.
지금까지 이해한 패킷의 흐름은 다음과 같다.
- 요청 과정
- 어플리케이션의 DNS 질의
- raw 테이블에서 zone 2로 기록된다.
- nat 테이블에 들어가 ISTIO_OUTPUT_DNS를 타고 15053으로 향한다.
- 에이전트(istio-proxy UID를 가짐)의 DNS 질의
- raw 테이블에서 zone 1로 기록된다.
- istio-proxy UID를 가지므로 바로 리턴된다.
- 시스템 리졸버의 DNS 질의
- 처음에는 잘 몰랐는데, 이것도 iptables의 영향을 당연히 받는다.
- 이때 엔보이 아웃바운드 핸들러인 7번 룰까지 들어가서 ISTIO_REDIRECT 체인으로 전달된다.
- 근데 이 체인에서는 tcp 통신만 취급하기 때문에 아무런 변경 없이 리턴된다!
- 어플리케이션의 DNS 질의
- 응답 과정
- 시스템 리졸버의 DNS 질의에 대한 응답
- 외부의 응답이라 PREROUTING에서 들어온다.
- 이때 raw 테이블에서 zone1로 기록되기에, 이후 nat 테이블은 컨트랙으로 넘어간다.
- 에이전트(istio-proxy UID를 가짐)의 DNS 질의에 대한 응답
- 에이전트의 응답 주소가 127.0.0.1에 해당해 6번 룰에서 리턴된다.
- 참고로 6번 룰이 여기 빼고 적용되는 것을 못 봤다.
- 어플리케이션의 DNS 질의에 대한 응답
- raw 테이블에서 zone 2로 기록되기에 nat 테이블을 거치지 않고 넘어간다.
- 시스템 리졸버의 DNS 질의에 대한 응답
컨트랙의 동작을 잘 몰라서 처음에 이해하는 게 너무 어려웠다.
사실 지금도 명확하게 안다고 이야기는 못 하겠지만, 이렇게 해석했을 때 패킷 카운트의 변화가 잘 해석돼서, 이게 맞다고 생각한다.
conntrack -L -p udp
bpftrace -e 'tracepoint:syscalls:sys_enter_connect { printf("%d %s\n", pid, comm); }'
정확하게 추적해보려고 여러 시도를 해봤다..
시스템 리졸버가 있어서 트래픽이 엄청 꼬였지, 사실 리졸버 없이 바로 외부 네임서버로 질의가 이뤄졌다면 패킷 흐름은 훨씬 단순화됐을 것으로 보인다.
인바운드 트래픽
패킷 수집은 계속 같은 방법을 활용하고, 대신 어떤 위치에서 요청을 하냐에 따라서만 차이를 뒀다.
# vm에서
tcpdump -i any -c 100 -w /home/ubuntu/capture2.pcap
# 로컬에서
scp -i infra-terraform/key.pem -r ubuntu@$FORUM:/home/ubuntu/capture2.pcap .
외부에서 먼저 들어오는 요청은 iptables의 영향을 받는 과정을 추적하는 게 조금 까다롭다.
왜냐하면 tcpdump는 인터페이스를 지나는 시점의 트래픽을 뜯어내기 때문이다.
외부에서 들어오는 트래픽은 PREROUTING 테이블에 진입하기 이전에 수집된다.
반면 내부에서 나가는 트래픽은 인터페이스로 가기 전에 OUTPUT테이블을 거치고 수집된다.
그래서 이 둘 간의 차이를 인식하고 패킷을 들여다봐야 한다.
먼저 패킷 카운트는 이렇게 일어난다.
인바운드 트래픽은 확인이 편한 편이다.
응답 패킷은 컨트랙돼서 iptables에서 추적되지 않는다.
외부의 요청은 특정 포트가 아닐 때는 무조건 ISTIO_IN_REDIRECT로 빠지는데 이때 envoy의 인바운드 핸들러 포트 15006으로 빠진다.
그리고 엔보이는 127.0.0.6의 주소를 가지고 어플리케이션에 요청을 하기에 PREROUTING 체인을 거치지 않고 전달된다.
127.0.0.6은 ISTIO_OUTPUT 체인의 첫번째 룰로 바로 리턴되며, 결과적으로 어플리케이션에 바로 전달될 것이다.
여기에서 엔보이 설정도 잠시 확인해본다.
curl localhost:15000/config_dump | istioctl pc listener -f -
istioctl을 vm에 설치하면 설정을 그나마? 편하게 볼 수 있다.
보다시피 0.0.0.0:15006은 목적지가 InboundPassthroughCluster라고 표시된다.
해당 포트의 설정을 조금 더 자세히 보자면, 일단 해당 포트에 대한 리스너 이름은 virtualInbound이다.
그리고 리스너의 필터체인에 리스트로 여러 체인이 걸려 있는데, 각각은 filterChainMatch로 매칭된 트래픽을 처리할 방침을 정해두고 있다.
맨 위, 인바운드 포트로 직빵으로 날리는 요청에 대해서는 virtualInbound-blackhole, 즉 블랙홀로 빠뜨려버린다!
이후 체인들은 대체로 비슷하게 생겼다.
근데 아무튼 InboundPassthroughCluster 라우트 설정을 적용하며, 이때 가상호스트 이름을 inbound|http|0
으로 적용한다.
InboundPassthroughCluster라는 클러스터로 요청이 갈 것이란 것도 확인할 수 있다.
그럼 라우트 설정도 확인해보자.
curl localhost:15000/config_dump | istioctl pc -f - route
중간에 위에서 봤던 InboundPassthroughCluster가 확인된다!
가상호스트 이름도 일치한다.
여기 들어있는 설정은 위에서 본 리스너 설정에 일부만 표시된다.
route라는 게 리스너 필터 체인 중 HCM에 대한 설정이니 사실 당연한 것 같다.
다음은 클러스터에 대해 살펴본다.
엔보이의 작동 구조를 안다면, 결국 자신이 프록싱하는 어플리케이션 역시 클러스터 중 하나라는 것을 사실 쉽게 알 수 있다.
curl localhost:15000/config_dump | istioctl pc -f - cluster --fqdn InboundPassthroughCluster
127.0.0.6이란 주소가 드디어 등장했다!
클러스터 중 ORIGINAL_DST
타입은 들어온 목적지 주소 그대로 보낸다는 의미를 가지고 있다.[5]
여기에 출발지 주소를 127.0.0.6으로, 즉 엔보이 자기 자신의 주소를 저렇게 매긴다.
그래서 어플리케이션에서 소스 ip를 까보면 죄다 127.0.0.6이 나오는 것이다.
번외 - 엔보이의 업스트림 커넥션
처음에 명확하게 인식하지 못해 헤맨 포인트가 있었는데, 서비스에서 가상머신으로 반복접근할 때 PREROUTING이 증가하지 않는 이슈가 있었다.
curl -is $FORUM:8080/api/users | grep HTTP
k -n istioinaction exec -ti catalog-5899bbc954-xpqqg -- curl forum.forum-services/api/users
내 로컬에서 그냥 요청을 보낼 때는 문제없이 지속적으로 PREROUTING으로 패킷이 드나드는 것이 확인됐다.
그러나 서비스에서 실행할 때는 PREROUTING의 패킷이 처음 요청을 보낼 때만 카운트되고 이후에는 변화 없이 트래픽이 반환됐다.
물론 실제 요청은 가상머신에서 실행되는 것이 확실했다.
conntrack을 지우고 다시 했을 때 제대로 카운트가 되는 것을 확인했다.
찾아보니 컨트랙 정보가 저장될 때, nat와 같은 iptables 단에서 설정되는 정보들을 저장하기 때문에 같은 정보가 들어오면 컨트랙 모듈이 nat 테이블을 우회할 수 있도록 해준다는 모양이다.
패킷을 뜯었을 때 조금 더 명확하게 할 수 있었는데, 일단 엔보이 기본 설정이 KeepAlive를 통해 TCP 커넥션을 유지한다.
덕분에 클러스터에서 가상머신으로, 심지어 가상머신에서 외부로 요청을 보낼 때도 지속적으로 같은 포트를 이용해서 통신한다..
192.168.10.200:55320은 vm의 포럼 서비스에서 외부의 jsonplaceholder 사이트로 보내는 요청이다.
근데 이게 내가 요청을 3번을 보내는 동안 계속 반복돼서 사용됐다.
내 로컬에서 요청을 보낼 때는 내 로컬이 keep alive 헤더를 포함시키지 않고 커넥션 풀을 유지하지도 않기 때문에 매번 새로운 커넥션으로 인식이 된 것이다.
아웃바운드 트래픽
그럼 나가는 트래픽은 어떠한가?
동서 게이트웨이에 통신할 때는 mtls가 적용되기 때문에, 커다란 크기를 가진 TLS 통신 패킷이 보인다.
참고로 ISTIO_OUTPUT_DNS 체인은 무조건 들어가게 돼있다.
안 속 룰에 조건이 걸려 있어 DNS 질의가 아니면 그냥 나온다.
그리고 동서 게이트웨이에 이미 컨트랙이 걸려 있어서 그런지 테이블에서 패킷이 추적되지 않은 것으로 보인다.
어차피 엔보이로 들어가는 트래픽의 흐름이 핵심이기 때문에 해당 패킷이 카운트되지 않는 것은 신경 쓰지 않았다.
여태 봤던 패킷 흐름 중에선 그래도 가장 간단한 것 같기도..?
아무튼 ISTIO_OUTPUT 체인의 여러 룰에 하나도 걸리지 않은 패킷은 마지막 룰인 ISTIO_REDIRECT 체인으로 가게 된다.
그리고 해당 체인은 15001 포트로 패킷을 리다이렉트시키기에 결과적으로 엔보이로 트래픽이 흘러가는 것을 확인할 수 있다.
15001은 엔보이의 아웃바운드 핸들러 포트로, 해당 포트로 들어온 패킷을 기반으로 외부 통신을 진행한다.
그럼 정말 15001에선 무슨 일이 일어나는가?
curl localhost:15000/config_dump | istioctl pc -f - listener
보다시피 엔보이의 리스너 설정에서는 PassthroughCluster를 목적지로 삼고 있는 것을 볼 수 있다.
조금 더 자세히 보면, 트래픽 방향은 OUTBOUND로 나오며 라우트 설정이 없는 것을 확인할 수 있다.
그저 패스스루클러스터로 보내기만 하는 것으로 보이나, 실상 여기에서 다른 리스너 매칭이 이뤄진다.[6]
이스티오 문서에서는 자세히 언급이 없어서 구체적으로 어떤 설정 때문에 이게 가능한지 찾아봤다.[7]
15001 포트를 리스닝하는 이 virturlOutbound는 useOriginalDst값이 참이다.
이로부터 받을 때는 15006 포트 소켓으로 패킷을 받았지만 원래 목적지 주소에 해당하는 리스너에게 커넥션을 그냥 넘겨버린다.
해당하는 조건에 매칭되는 리스너가 없다면 비로서 위 패스스루 클러스터로 빠지는 구조이다.
패스스루 클러스터로 넘어가면 이때는 그냥 주소 기반으로 트래픽을 날려버린다.
이렇게 될 경우 라우트 설정에 들어간 세밀한 트래픽 제어 설정은 기대할 수 없게 될 것이다.
자기 자신 호출 트래픽
마지막으로 볼 것은 처음 ISTIO_OUTPUT 테이블을 봤을 때 가장 이해를 어렵게 만드는 2번 룰에 대한 패킷이다.
2번 룰은 자기 자신을 호출할 때 발동된다.
다시 2번의 조건을 상기시키자면,
- istio-proxy가 보낸 패킷이어야 한다.
- 127.0.0.1이 아니어야 한다.
- lo 인터페이스를 타야 한다.
- 목적지 포트가 53, 15008가 아니어야 한다.
2번과 3번 조건은 꽤나 쉬워보는 게 127.0.0.0/8
내부는 전부 lo 인터페이스를 타니 저 범위에 해당하는 임의의 주소로 요청을 보내면 될 것 같지만..
그러한 요청을 어떻게 해야 istio-proxy가 날리는가가 관건이다.
그냥 curl 127.0.2.0
이런 식으로 요청을 날리면, 해당 요청은 15001을 타고 엔보이가 요청을 수행하기 이전에 3번 룰에 의해 리턴되어버린다.
istio-proxy가 아니면서 바로 lo 인터페이스를 타버리기 때문에 엔보이가 관리하지 않게 된다는 것.
그렇다면 자기 자신을 가리키는 다른 인터페이스의 주소라면 어떨까?
curl 192.168.10.200
으로 요청을 보내보면 여전히 원하는 매칭은 이뤄지지 않는 것을 확인할 수 있다.
이유는 사실 간단하다.
라우팅 테이블 룰 상에서는 local이 가장 먼저 적용되는데, 여기에 각 인터페이스 별 자신이 가진 주소가 적혀있다.
이 테이블에 걸리면 lo 인터페이스를 타게 되므로 해당 요청도 정답이 아니다.
이게 가능한 케이스는 서비스 메시 내에서 부여된 자기 자신의 호스트나 주소로 요청을 보내는 것이다.
왜 이렇게 되는지 이제부터 보도록 한다.
vm에는 forum이 가동되고 있어 curl forum
과 같은 식으로 요청을 날렸다.
15001을 탔다가 15006을 탔다가 난리 부르스를 피는 게 보인다!
드디어 2번 룰에 의해 패킷이 ISTIO_IN_REDIRECT 체인에 들어가게 됐다.
해당 체인에 들어가면 15006 포트로 들어가게 되므로 위와 같은 결과가 나온 것이다.
dns 관련 패킷을 제외하고, 인바운드를 처리하는 ISTIO_IN_REDIRECT체인과 아웃바운드를 처리하는 ISTIO_REDIRECT 체인이 카운트되는 것을 확인할 수 있다.
이 케이스는 홉이 많아서 직관적으로는 이냥저냥이어도 막상 패킷을 뜯어보면 또 복잡하다.
순서대로 정리해보자면,
- 메시 내 자신의 호스트 이름을 질의하면 메시 내 서비스 IP 주소(10.43.196.42.80)가 나온다.
- 어플리케이션에서 이 주소로 요청을 보내면 당연히 아웃바운드 포트로 엔보이가 받는다.
- 엔보이는 EDS에 가상 주소가 아닌 실제 주소를 알고 있고, 이를 통해 192.168.10.200으로 요청을 보낸다.
- 이때 2번 룰에 걸리는 조건이 충족된다!
- 엔보이가 보냈으니 istio-proxy UID
- 127.0.0.1 아님
- 라우팅 테이블 상 어떤 인터페이스에서든 자기 자신을 가리키는 주소는 lo 인터페이스로 들어감
- 당연히 목적지 포트는 53, 15008이 아님
- ISTIO_IN_REDIRECT 체인에 들어가게 돼 결국 인바운드 포트로 엔보이에 다시 들어간다.
- 인바운드를 받았으니 당연히 요청은 어플리케이션으로 들어간다.
응답은 전부 역순이다.
패킷 덤프 상에서 중간에 404 응답이 없어서 헷갈렸는데, 이건 중간에 mtls 통신이 이뤄져서 그렇다.
구체적으로는 엔보이가 혼자 패킷을 주고받는 과정에서 mtls가 이뤄진다.
해당 내용물을 까볼 순 없어도 PSH(push 제어 플래그. 데이터를 바로 넘겨라)를 통해 통신이 되고 있단 것은 확인할 수 있다.
결과적으로, 자기 자신을 호출하되 서비스 메시 내부의 트래픽으로서 자신을 호출한다면 이는 결국 메시의 트래픽 제어를 받게 된다.
인바운드 핸들러를 거쳐서 자신에게 돌아오기 때문이다!
결론
원래 생각은 iptables 조금 해석해보고 말 생각이었는데, 도무지 이해가 안 되는 룰이 있어서 결국 까보게 됐다.
iptables에서 테이블 간 적용 순서가 항상 헷갈렸는데, 이번에 이렇게 삽질을 해대니 이제 그 흐름이 살짝 체화됐다고 해야 하나, 자연스럽게 느껴진다.
iptables의 룰이 상당히 복잡하게 짜여진 것으로 보이지만, 이러한 규칙을 적용했기에 다양한 상황의 패킷을 처리할 수 있게 된다.
사실 자기 자신을 호출하는 트래픽을 제어하지 않을 요량이었다면 룰은 훨씬 간소화됐을 것이다.
그럼에도 메시 내 트래픽 제어라는 서비스 메시의 기능을 엄격하게 지켜내는 방향으로 철저하게 설계했고, 또 그렇게 구현됐기에 이스티오가 또 이렇게 유명해질 수 있었던 게 아닐까 생각한다.
iptables 공부가 꽤 많이 된 것 같다..
uid나 gid로 매칭이 가능한 이유가 이렇다고 한다.
하위 문서
이름 | is-folder | index | noteType | created |
---|---|---|---|---|
E-서버리스 실습 | - | - | topic/explain | 2024-06-27 |
E-도커 파일 구조 탐색 | - | - | topic/explain | 2024-08-05 |
E-로컬 ssh 서버 세팅 | - | - | topic/explain | 2024-07-13 |
E-쿠버네티스 클러스터 구축 | - | - | topic/explain | 2024-07-29 |
E-레디네스 프로브와 레디네스 게이트 | - | - | topic/explain | 2024-08-15 |
E-initialDelaySeconds가 아니라 스타트업 프로브가 필요한 이유 | - | - | topic/explain | 2024-08-25 |
E-초기화 컨테이너보다 앞서는 사이드카 컨테이너 | - | - | topic/explain | 2024-08-30 |
E-디플로이먼트 조작 | - | - | topic/explain | 2024-09-14 |
E-NFS 볼륨, 스토리지 클래스 설정 | - | - | topic/explain | 2024-10-17 |
E-AWS KRUG 핸즈온 실습 - ECS | false | - | topic | 2024-11-03 |
E-컨테이너 오라클 dbms 설치 | false | - | topic/explain | 2024-11-26 |
E-리눅스 dbeaver 설치 | false | - | topic/explain | 2024-11-26 |
E-오라클 사용자 및 테이블 스페이스 생성 | false | - | topic/explain | 2024-11-26 |
E-파드의 readinessProbe와 디플로이먼트의 minReadySeconds의 차이 | false | - | topic/explain | 2024-12-26 |
E-잡 패턴 실습 | false | - | topic | 2024-12-28 |
E-iptables와 nftables의 차이 | false | - | topic/explain | 2024-12-30 |
E-바인딩과 하드 링크의 차이 | false | - | topic/explain | 2025-01-16 |
E-emptyDir 제한 | false | - | topic/explain | 2025-01-16 |
E-쿠버네티스 인증 실습 | false | - | topic/explain | 2025-01-21 |
E-api 서버 감사 | false | - | topic/explain | 2025-01-21 |
E-파드 마운팅 recursiveReadOnly | false | - | topic/explain | 2025-02-27 |
E-projected 볼륨 - 동적 업데이트, 중복 활용 | false | - | topic/explain | 2025-03-10 |
E-쿠버 RBAC 권한 상승 방지 실습 | false | - | topic/explain | 2025-03-16 |
E-Kyverno 기본 실습 | false | - | topic/explain | 2025-03-17 |
E-검증 승인 정책 실습 | false | - | topic/explain | 2025-03-17 |
E-nodeName으로 스케줄링 실습 | false | - | topic/explain | 2025-03-19 |
E-openssl을 이용한 인증서 생성 실습 | false | - | topic/explain | 2025-03-22 |
E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습 | false | - | topic/explain | 2025-03-30 |
E-이스티오의 데이터 플레인 트래픽 세팅 원리 | false | 1 | topic/explain | 2025-05-27 |
E-이스티오 가상머신 통합 | false | 1 | topic/explain | 2025-06-01 |
E-이스티오 설정 트러블슈팅하기 | false | 2 | topic/explain | 2025-05-18 |
E-deb 파일 뜯어보기 | false | 2 | topic/explain | 2025-06-01 |
E-앰비언트 모드 헬름 세팅 | false | 2 | topic/explain | 2025-06-03 |
E-이스티오 컨트롤 플레인 성능 최적화 | false | 3 | topic/explain | 2025-05-18 |
E-이스티오 DNS 프록시 동작 | false | 3 | topic/explain | 2025-06-01 |
E-앰비언트 ztunnel 트래픽 경로 분석 | false | 3 | topic/explain | 2025-06-07 |
E-이스티오 컨트롤 플레인 메트릭 | false | 4 | topic/explain | 2025-05-18 |
E-앰비언트 모드에서 메시 기능 활용 | false | 5 | topic/explain | 2025-06-07 |
관련 문서
지식 문서, EXPLAIN
이름0 | is-folder | 생성 일자 |
---|
기타 문서
Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완이름0 | 코드 | 타입 | 생성 일자 |
---|
참고
https://www.linux.co.kr/bbs/board.php?bo_table=lecture&wr_id=3763 ↩︎
https://ssup2.github.io/blog-software/docs/theory-analysis/linux-conntrack/ ↩︎
https://jimmysong.io/en/blog/sidecar-injection-iptables-and-traffic-routing/#understand-outbound-handler ↩︎
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-enum-config-cluster-v3-cluster-discoverytype ↩︎
https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/ ↩︎
https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener.proto#config-listener-v3-listener ↩︎